package com.onlylemi.mapview.library;

import com.onlylemi.mapview.library.layer.MapBaseLayer;
import com.onlylemi.mapview.library.layer.MapLayer;
import com.onlylemi.mapview.library.utils.MapMath;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.surfaceprovider.SurfaceProvider;
import ohos.agp.graphics.SurfaceOps;
import ohos.agp.render.Canvas;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Point;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.multimodalinput.event.TouchEvent;

import java.util.ArrayList;
import java.util.List;

public class MapView extends SurfaceProvider implements SurfaceOps.Callback, Component.TouchEventListener {
    private static final String TAG = "MapView";
    private static final int TOUCH_STATE_NO = 0; // no touch
    private static final int TOUCH_STATE_SCROLL = 1; // scroll(one point)
    private static final int TOUCH_STATE_SCALE = 2; // scale(two points)
    private static final int TOUCH_STATE_ROTATE = 3; // rotate(two points)
    private static final int TOUCH_STATE_TWO_POINTED = 4; // two points touch
    private SurfaceOps holder;
    private MapViewListener mapViewListener = null;
    private boolean isMapLoadFinish = false;
    private List<MapBaseLayer> layers; // all layers
    private MapLayer mapLayer;
    private float minZoom = 0.5f;
    private float maxZoom = 3.0f;
    private Point startTouch = new Point();
    private Point lastMove = new Point();
    private Point mid = new Point();
    private Matrix saveMatrix = new Matrix();
    private Matrix currentMatrix = new Matrix();
    private float currentZoom = 1.0f;
    private float saveZoom = 0f;
    private float currentRotateDegrees = 0.0f;
    private float saveRotateDegrees = 0.0f;
    private int currentTouchState = MapView.TOUCH_STATE_NO; // default touch state

    private float oldDist = 0;
    private float oldDegree = 0;
    private boolean isScaleAndRotateTogether = false;

    private PixelMap bitmap;

    private MapTask mapTask;

    public MapView(Context context) {
        this(context, null);
    }

    public MapView(Context context, AttrSet attrSet) {
        this(context, attrSet, null);
    }

    public MapView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        setTouchEventListener(this);
        initMapView();
    }

    /**
     * init mapview
     */
    private void initMapView() {
        getSurfaceOps().get().addCallback(this);

        layers = new ArrayList<MapBaseLayer>() {
            @Override
            public boolean add(MapBaseLayer layer) {
                if (layers.size() != 0) {
                    if (layer.level >= this.get(this.size() - 1).level) {
                        super.add(layer);
                    } else {
                        for (int i = 0; i < layers.size(); i++) {
                            if (layer.level < this.get(i).level) {
                                super.add(i, layer);
                                break;
                            }
                        }
                    }
                } else {
                    super.add(layer);
                }
                return true;
            }
        };
    }

    @Override
    public void surfaceCreated(SurfaceOps surfaceOps) {
        this.holder = surfaceOps;

        if (isMapLoadFinish) {
            return;
        }
        if (bitmap != null) {
            if (mapLayer == null) {
                mapLayer = new MapLayer(MapView.this);

                // add map image layer
                layers.add(mapLayer);
            }
            mapLayer.setImage(bitmap);
            if (mapViewListener != null) {

                // load map success, and callback
                mapViewListener.onMapLoadSuccess();
            }
            isMapLoadFinish = true;
            refresh();
        } else {
            if (mapViewListener != null) {
                mapViewListener.onMapLoadFail();
            }
        }
    }

    @Override
    public void surfaceChanged(SurfaceOps surfaceOps, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(SurfaceOps surfaceOps) {

    }

    /**
     * reload mapview
     */
    public void refresh() {
        if (mapTask == null) {
            mapTask = new MapTask();
        }
        addDrawTask(mapTask);
    }

    private class MapTask implements DrawTask {
        @Override
        public void onDraw(Component component, Canvas canvas) {
            if (holder != null) {
                holder.lockCanvas();
                canvas.drawColor(-1, Canvas.PorterDuffMode.SRC_OVER);
                if (isMapLoadFinish) {
                    for (MapBaseLayer layer : layers) {
                        if (layer.isVisible) {
                            layer.draw(canvas, currentMatrix, currentZoom, currentRotateDegrees);
                        }
                    }
                }
                holder.unlockCanvasAndPost(canvas);
            }
        }
    }

    /**
     * 加载图片
     *
     * @param bitmap
     */
    public void loadMap(PixelMap bitmap) {
        this.bitmap = bitmap;
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (!isMapLoadFinish) {
            return false;
        }

        float newDist;
        float newDegree;

        switch (event.getAction() & 0xff) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                saveMatrix.setMatrix(currentMatrix);
                startTouch.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
                lastMove.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
                currentTouchState = MapView.TOUCH_STATE_SCROLL;
                break;
            case TouchEvent.OTHER_POINT_DOWN:
                if (event.getPointerCount() == 2) {
                    saveMatrix.setMatrix(currentMatrix);
                    saveZoom = currentZoom;
                    saveRotateDegrees = currentRotateDegrees;
                    startTouch.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
                    currentTouchState = MapView.TOUCH_STATE_TWO_POINTED;

                    mid = midPoint(event);
                    oldDist = distance(event, mid);
                    oldDegree = rotation(event, mid);
                }
                break;
            case TouchEvent.PRIMARY_POINT_UP:
                if (withFloorPlan(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY())) {

                    // layers on touch
                    for (MapBaseLayer layer : layers) {
                        layer.onTouch(event);
                    }
                }
                currentTouchState = MapView.TOUCH_STATE_NO;
                break;
            case TouchEvent.OTHER_POINT_UP:
                currentTouchState = MapView.TOUCH_STATE_NO;
                break;
            case TouchEvent.POINT_MOVE:
                if (Math.abs(event.getPointerPosition(0).getX() - lastMove.getPointX()) > 0
                        && Math.abs(event.getPointerPosition(0).getY() - lastMove.getPointY()) > 0) {
                    switch (currentTouchState) {
                        case MapView.TOUCH_STATE_SCROLL:
                            currentMatrix.setMatrix(saveMatrix);
                            currentMatrix.postTranslate(event.getPointerPosition(0).getX() - startTouch.getPointX(),
                                    event.getPointerPosition(0).getY() - startTouch.getPointY());
                            refresh();
                            break;
                        case MapView.TOUCH_STATE_TWO_POINTED:
                            if (!isScaleAndRotateTogether) {
                                float x = oldDist;
                                float y = MapMath.getDistanceBetweenTwoPoints(event.getPointerPosition(0).getX(),
                                        event.getPointerPosition(0).getY(), startTouch.getPointX(),
                                        startTouch.getPointY());
                                float z = distance(event, mid);
                                float cos = (x * x + y * y - z * z) / (2 * x * y);
                                float degree = (float) Math.toDegrees(Math.acos(cos));

                                if (degree < 120 && degree > 45) {
                                    oldDegree = rotation(event, mid);
                                    currentTouchState = MapView.TOUCH_STATE_ROTATE;
                                } else {
                                    oldDist = distance(event, mid);
                                    currentTouchState = MapView.TOUCH_STATE_SCALE;
                                }
                            } else {
                                currentMatrix.setMatrix(saveMatrix);
                                newDist = distance(event, mid);
                                newDegree = rotation(event, mid);

                                float rotate = newDegree - oldDegree;
                                float scale = newDist / oldDist;
                                if (scale * saveZoom < minZoom) {
                                    scale = minZoom / saveZoom;
                                } else if (scale * saveZoom > maxZoom) {
                                    scale = maxZoom / saveZoom;
                                }
                                currentZoom = scale * saveZoom;
                                currentRotateDegrees = (newDegree - oldDegree + currentRotateDegrees)
                                        % 360;
                                currentMatrix.postScale(scale, scale, mid.getPointX(), mid.getPointY());
                                currentMatrix.postRotate(rotate, mid.getPointX(), mid.getPointY());
                                refresh();
                            }
                            break;
                        case MapView.TOUCH_STATE_SCALE:
                            currentMatrix.setMatrix(saveMatrix);
                            newDist = distance(event, mid);
                            float scale = newDist / oldDist;
                            if (scale * saveZoom < minZoom) {
                                scale = minZoom / saveZoom;
                            } else if (scale * saveZoom > maxZoom) {
                                scale = maxZoom / saveZoom;
                            }
                            currentZoom = scale * saveZoom;
                            currentMatrix.postScale(scale, scale, mid.getPointX(), mid.getPointY());
                            refresh();
                            break;
                        case MapView.TOUCH_STATE_ROTATE:
                            currentMatrix.setMatrix(saveMatrix);
                            newDegree = rotation(event, mid);
                            float rotate = newDegree - oldDegree;
                            currentRotateDegrees = (rotate + saveRotateDegrees) % 360;
                            currentRotateDegrees = currentRotateDegrees > 0 ? currentRotateDegrees
                                    : currentRotateDegrees + 360;
                            currentMatrix.postRotate(rotate, mid.getPointX(), mid.getPointY());
                            refresh();
                            break;
                        default:
                            break;
                    }
                    lastMove.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
                }
                break;
            default:
                break;
        }
        return true;
    }

    /**
     * set mapview listener
     *
     * @param mapViewListener
     */
    public void setMapViewListener(MapViewListener mapViewListener) {
        this.mapViewListener = mapViewListener;
    }

    /**
     * convert coordinate of map to coordinate of screen
     *
     * @param x
     * @param y
     * @return float[]
     */
    public float[] convertMapXYToScreenXY(float x, float y) {
        Matrix invertMatrix = new Matrix();
        float[] value = {x, y};
        currentMatrix.invert(invertMatrix);
        invertMatrix.mapPoints(value);
        return value;
    }

    /**
     * map is/not load finish
     *
     * @return isMapLoadFinish
     */
    public boolean isMapLoadFinish() {
        return isMapLoadFinish;
    }

    /**
     * add layer
     *
     * @param layer
     */
    public void addLayer(MapBaseLayer layer) {
        if (layer != null) {
            layers.add(layer);
        }
    }

    /**
     * get all layers
     *
     * @return layers
     */
    public List<MapBaseLayer> getLayers() {
        return layers;
    }

    /**
     * translate
     *
     * @param x
     * @param y
     */
    public void translate(float x, float y) {
        currentMatrix.postTranslate(x, y);
    }

    /**
     * set point to map center
     *
     * @param x
     * @param y
     */
    public void mapCenterWithPoint(float x, float y) {
        float[] goal = {x, y};
        currentMatrix.mapPoints(goal);

        float deltaX = (float) getWidth() / 2 - goal[0];
        float deltaY = (float) getHeight() / 2 - goal[1];
        currentMatrix.postTranslate(deltaX, deltaY);
    }

    public float getCurrentRotateDegrees() {
        return currentRotateDegrees;
    }

    /**
     * set rotate degrees
     *
     * @param degrees
     */
    public void setCurrentRotateDegrees(float degrees) {
        mapCenterWithPoint(getMapWidth() / 2, getMapHeight() / 2);
        setCurrentRotateDegrees(degrees, (float) getWidth() / 2, (float) getHeight() / 2);
    }

    /**
     * set rotate degrees
     *
     * @param degrees
     * @param x
     * @param y
     */
    public void setCurrentRotateDegrees(float degrees, float x, float y) {
        currentMatrix.postRotate(degrees - currentRotateDegrees, x, y);

        currentRotateDegrees = degrees % 360;
        currentRotateDegrees = currentRotateDegrees > 0 ? currentRotateDegrees
                : currentRotateDegrees + 360;
    }

    /**
     * getCurrentZoom
     *
     * @return currentZoom
     */
    public float getCurrentZoom() {
        return currentZoom;
    }

    /**
     * isScaleAndRotateTogether
     *
     * @return isScaleAndRotateTogether
     */
    public boolean isScaleAndRotateTogether() {
        return isScaleAndRotateTogether;
    }

    /**
     * setting scale&rotate is/not together on touch
     *
     * @param scaleAndRotateTogether
     */
    public void setScaleAndRotateTogether(boolean scaleAndRotateTogether) {
        isScaleAndRotateTogether = scaleAndRotateTogether;
    }

    /**
     * setMaxZoom
     *
     * @param maxZoom
     */
    public void setMaxZoom(float maxZoom) {
        this.maxZoom = maxZoom;
    }

    /**
     * setMinZoom
     *
     * @param minZoom
     */
    public void setMinZoom(float minZoom) {
        this.minZoom = minZoom;
    }

    /**
     * setCurrentZoom
     *
     * @param zoom
     */
    public void setCurrentZoom(float zoom) {
        setCurrentZoom(zoom, (float) getWidth() / 2, (float) getHeight() / 2);
    }

    /**
     * setCurrentZoom
     *
     * @param zoom
     * @param x
     * @param y
     */
    public void setCurrentZoom(float zoom, float x, float y) {
        currentMatrix.postScale(zoom / this.currentZoom, zoom / this.currentZoom, x, y);
        this.currentZoom = zoom;
    }

    /**
     * midPoint
     *
     * @param event
     * @return Point
     */
    private Point midPoint(TouchEvent event) {
        return MapMath.getMidPointBetweenTwoPoints(event.getPointerPosition(0).getX(),
                event.getPointerPosition(0).getY(),
                event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY());
    }

    /**
     * distance
     *
     * @param event
     * @param mid
     * @return distance
     */
    private float distance(TouchEvent event, Point mid) {
        return MapMath.getDistanceBetweenTwoPoints(event.getPointerPosition(0).getX(),
                event.getPointerPosition(0).getY(), mid.getPointX(), mid.getPointY());
    }

    /**
     * rotation
     *
     * @param event
     * @param mid
     * @return rotation
     */
    private float rotation(TouchEvent event, Point mid) {
        return MapMath.getDegreeBetweenTwoPoints(event.getPointerPosition(0).getX(),
                event.getPointerPosition(0).getY(), mid.getPointX(), mid.getPointY());
    }

    /**
     * point is/not in floor plan
     *
     * @param x
     * @param y
     * @return boolean
     */
    public boolean withFloorPlan(float x, float y) {
        float[] goal = convertMapXYToScreenXY(x, y);
        return goal[0] > 0 && goal[0] < mapLayer.getImage().getImageInfo().size.width && goal[1] > 0
                && goal[1] < mapLayer.getImage().getImageInfo().size.height;
    }

    /**
     * getMapWidth
     *
     * @return width
     */
    public float getMapWidth() {
        return mapLayer.getImage().getImageInfo().size.width;
    }

    /**
     * getMapHeight
     *
     * @return height
     */
    public float getMapHeight() {
        return mapLayer.getImage().getImageInfo().size.height;
    }
}
